/*
 * $Id$
 *
 * Copyright 2009 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */
package org.jdesktop.swingx.sort;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

import javax.swing.DefaultRowSorter;
import javax.swing.SortOrder;

import org.jdesktop.swingx.renderer.StringValue;
import org.jdesktop.swingx.renderer.StringValues;
import org.jdesktop.swingx.util.Contract;

/**
 * A default SortController implementation used as parent class for concrete
 * SortControllers in SwingX.
 * <p>
 * 
 * Additionally, this implementation contains a fix for core <a
 * href=http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6894632>Issue
 * 6894632</a>. It guarantees to only touch the underlying model during
 * sort/filter and during processing the notification methods. This implies that
 * the conversion and size query methods are valid at all times outside the
 * internal updates, including the critical period (in core with undefined
 * behaviour) after the underlying model has changed and before this sorter has
 * been notified.
 * 
 * @author Jeanette Winzenburg
 */
public abstract class DefaultSortController<M> extends DefaultRowSorter<M, Integer> implements SortController<M> {

	/**
	 * Comparator that uses compareTo on the contents.
	 */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public static final Comparator COMPARABLE_COMPARATOR = new ComparableComparator();

	private final static SortOrder[] DEFAULT_CYCLE = new SortOrder[] { SortOrder.ASCENDING, SortOrder.DESCENDING };

	private List<SortOrder> sortCycle;

	private boolean sortable;

	private StringValueProvider stringValueProvider;

	protected int cachedModelRowCount;

	public DefaultSortController() {
		super();
		setSortable(true);
		setSortOrderCycle(DEFAULT_CYCLE);
		setSortsOnUpdates(true);
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * 
	 */
	@Override
	public void setSortable(boolean sortable) {
		this.sortable = sortable;
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * 
	 */
	@Override
	public boolean isSortable() {
		return sortable;
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * 
	 */
	@Override
	public void setSortable(int column, boolean sortable) {
		super.setSortable(column, sortable);
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * 
	 */
	@Override
	public boolean isSortable(int column) {
		if (!isSortable())
			return false;
		return super.isSortable(column);
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * 
	 * Overridden - that is completely new implementation - to get first/next
	 * SortOrder from sort order cycle. Does nothing if the cycle is empty.
	 */
	@Override
	public void toggleSortOrder(int column) {
		checkColumn(column);
		if (!isSortable(column))
			return;
		SortOrder firstInCycle = getFirstInCycle();
		// nothing to toggle through
		if (firstInCycle == null)
			return;
		List<SortKey> keys = new ArrayList<SortKey>(getSortKeys());
		SortKey sortKey = SortUtils.getFirstSortKeyForColumn(keys, column);
		if (keys.indexOf(sortKey) == 0) {
			// primary key: in this case we'll use next sortorder in cylce
			keys.set(0, new SortKey(column, getNextInCycle(sortKey.getSortOrder())));
		} else {
			// all others: make primary with first sortOrder in cycle
			keys.remove(sortKey);
			keys.add(0, new SortKey(column, getFirstInCycle()));
		}
		if (keys.size() > getMaxSortKeys()) {
			keys = keys.subList(0, getMaxSortKeys());
		}
		setSortKeys(keys);
	}

	/**
	 * Returns the next SortOrder relative to the current, or null if the sort
	 * order cycle is empty.
	 * 
	 * @param current
	 *            the current SortOrder
	 * @return the next SortOrder to use, may be null if the cycle is empty.
	 */
	private SortOrder getNextInCycle(SortOrder current) {
		int pos = sortCycle.indexOf(current);
		if (pos < 0) {
			// not in cycle ... what to do?
			return getFirstInCycle();
		}
		pos++;
		if (pos >= sortCycle.size()) {
			pos = 0;
		}
		return sortCycle.get(pos);
	}

	/**
	 * Returns the first SortOrder in the sort order cycle, or null if empty.
	 * 
	 * @return the first SortOrder in the sort order cycle or null if empty.
	 */
	private SortOrder getFirstInCycle() {
		return sortCycle.size() > 0 ? sortCycle.get(0) : null;
	}

	private void checkColumn(int column) {
		if (column < 0 || column >= getModelWrapper().getColumnCount()) {
			throw new IndexOutOfBoundsException("column beyond range of TableModel");
		}
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * 
	 * PENDING JW: toggle has two effects: makes the column the primary sort
	 * column, and cycle through. So here we something similar. Should we?
	 * 
	 */
	@Override
	public void setSortOrder(int column, SortOrder sortOrder) {
		if (!isSortable(column))
			return;
		SortKey replace = new SortKey(column, sortOrder);
		List<SortKey> keys = new ArrayList<SortKey>(getSortKeys());
		SortUtils.removeFirstSortKeyForColumn(keys, column);
		keys.add(0, replace);
		// PENDING max sort keys, respect here?
		setSortKeys(keys);
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * 
	 */
	@Override
	public SortOrder getSortOrder(int column) {
		SortKey key = SortUtils.getFirstSortKeyForColumn(getSortKeys(), column);
		return key != null ? key.getSortOrder() : SortOrder.UNSORTED;
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * 
	 */
	@Override
	public void resetSortOrders() {
		if (!isSortable())
			return;
		List<SortKey> keys = new ArrayList<SortKey>(getSortKeys());
		for (int i = keys.size() - 1; i >= 0; i--) {
			SortKey sortKey = keys.get(i);
			if (isSortable(sortKey.getColumn())) {
				keys.remove(sortKey);
			}

		}
		setSortKeys(keys);

	}

	/**
	 * {@inheritDoc}
	 * <p>
	 */
	@Override
	public SortOrder[] getSortOrderCycle() {
		return sortCycle.toArray(new SortOrder[0]);
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 */
	@Override
	public void setSortOrderCycle(SortOrder... cycle) {
		Contract.asNotNull(cycle, "Elements of SortOrderCycle must not be null");
		// JW: not safe enough?
		sortCycle = Arrays.asList(cycle);
	}

	/**
	 * Sets the registry of string values. If null, the default provider is
	 * used.
	 * 
	 * @param registry
	 *            the registry to get StringValues for conversion.
	 */
	@Override
	public void setStringValueProvider(StringValueProvider registry) {
		this.stringValueProvider = registry;
		// updateStringConverter();
	}

	/**
	 * Returns the registry of string values.
	 * 
	 * @return the registry of string converters, guaranteed to never be null.
	 */
	@Override
	public StringValueProvider getStringValueProvider() {
		if (stringValueProvider == null) {
			stringValueProvider = DEFAULT_PROVIDER;
		}
		return stringValueProvider;
	}

	/**
	 * Returns the default cycle.
	 * 
	 * @return default sort order cycle.
	 */
	public static SortOrder[] getDefaultSortOrderCycle() {
		return Arrays.copyOf(DEFAULT_CYCLE, DEFAULT_CYCLE.length);
	}

	private static final StringValueProvider DEFAULT_PROVIDER = new StringValueProvider() {

		@Override
		public StringValue getStringValue(int row, int column) {
			return StringValues.TO_STRING;
		}

	};

	@SuppressWarnings({ "unchecked", "rawtypes" })
	private static class ComparableComparator implements Comparator {
		@Override
		public int compare(Object o1, Object o2) {
			return ((Comparable) o1).compareTo(o2);
		}
	}

	// -------------------------- replacing super for more consistent
	// conversion/rowCount behaviour

	/**
	 * {@inheritDoc}
	 * <p>
	 * 
	 * Overridden to use check against <code>getViewRowCount</code> for
	 * validity.
	 * 
	 * @see #getViewRowCount()
	 */
	@Override
	public int convertRowIndexToModel(int viewIndex) {
		if ((viewIndex < 0) || viewIndex >= getViewRowCount())
			throw new IndexOutOfBoundsException("valid viewIndex: 0 <= index < " + getViewRowCount() + " but was: " + viewIndex);
		try {
			return super.convertRowIndexToModel(viewIndex);
		} catch (Exception e) {
			// this will happen only if unsorted/-filtered and super
			// incorrectly access the model while it had been changed
			// under its feet
		}
		return viewIndex;
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * 
	 * Overridden to use check against <code>getModelRowCount</code> for
	 * validity.
	 * 
	 * @see #getModelRowCount()
	 */
	@Override
	public int convertRowIndexToView(int modelIndex) {
		if ((modelIndex < 0) || modelIndex >= getModelRowCount())
			throw new IndexOutOfBoundsException("valid modelIndex: 0 <= index < " + getModelRowCount() + " but was: "
					+ modelIndex);
		try {
			return super.convertRowIndexToView(modelIndex);
		} catch (Exception e) {
			// this will happen only if unsorted/-filtered and super
			// incorrectly access the model while it had been changed
			// under its feet
		}
		return modelIndex;
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * 
	 * Overridden to return the model row count which corresponds to the
	 * currently mapped model instead of accessing the model directly (as super
	 * does). This may differ from the "real" current model row count if the
	 * model has changed but this sorter not yet notified.
	 * 
	 */
	@Override
	public int getModelRowCount() {
		return cachedModelRowCount;
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * 
	 * Overridden to return the model row count if no filters installed,
	 * otherwise return super.
	 * 
	 * @see #getModelRowCount()
	 * 
	 */
	@Override
	public int getViewRowCount() {
		if (hasRowFilter())
			return super.getViewRowCount();
		return getModelRowCount();
	}

	/**
	 * @return
	 */
	private boolean hasRowFilter() {
		return getRowFilter() != null;
	}

	// ------------------ overridden notification methods: cache model row count
	@Override
	public void allRowsChanged() {
		cachedModelRowCount = getModelWrapper().getRowCount();
		super.allRowsChanged();
	}

	@Override
	public void modelStructureChanged() {
		super.modelStructureChanged();
		cachedModelRowCount = getModelWrapper().getRowCount();
	}

	@Override
	public void rowsDeleted(int firstRow, int endRow) {
		cachedModelRowCount = getModelWrapper().getRowCount();
		super.rowsDeleted(firstRow, endRow);
	}

	@Override
	public void rowsInserted(int firstRow, int endRow) {
		cachedModelRowCount = getModelWrapper().getRowCount();
		super.rowsInserted(firstRow, endRow);
	}

}
